package com.nineoldandroids.view.animation;

import android.graphics.Camera;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.os.Build;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Transformation;

import java.lang.ref.WeakReference;
import java.util.WeakHashMap;

/**
 * A proxy class to allow for modifying post-3.0 view properties on all pre-3.0
 * platforms. <strong>DO NOT</strong> wrap your views with this class if you are
 * using {@code ObjectAnimator} as it will handle that itself.
 */
public final class AnimatorProxy extends Animation {
	/** Whether or not the current running platform needs to be proxied. */
	public static final boolean NEEDS_PROXY = Integer
			.valueOf(Build.VERSION.SDK).intValue() < Build.VERSION_CODES.HONEYCOMB;

	private static final WeakHashMap<View, AnimatorProxy> PROXIES = new WeakHashMap<View, AnimatorProxy>();

	/**
	 * Create a proxy to allow for modifying post-3.0 view properties on all
	 * pre-3.0 platforms. <strong>DO NOT</strong> wrap your views if you are
	 * using {@code ObjectAnimator} as it will handle that itself.
	 *
	 * @param view
	 *            View to wrap.
	 * @return Proxy to post-3.0 properties.
	 */
	public static AnimatorProxy wrap(View view) {
		AnimatorProxy proxy = PROXIES.get(view);
		// This checks if the proxy already exists and whether it still is the
		// animation of the given view
		if (proxy == null || proxy != view.getAnimation()) {
			proxy = new AnimatorProxy(view);
			PROXIES.put(view, proxy);
		}
		return proxy;
	}

	private final WeakReference<View> mView;
	private final Camera mCamera = new Camera();
	private boolean mHasPivot;

	private float mAlpha = 1;
	private float mPivotX;
	private float mPivotY;
	private float mRotationX;
	private float mRotationY;
	private float mRotationZ;
	private float mScaleX = 1;
	private float mScaleY = 1;
	private float mTranslationX;
	private float mTranslationY;

	private final RectF mBefore = new RectF();
	private final RectF mAfter = new RectF();
	private final Matrix mTempMatrix = new Matrix();

	private AnimatorProxy(View view) {
		setDuration(0); // perform transformation immediately
		setFillAfter(true); // persist transformation beyond duration
		view.setAnimation(this);
		mView = new WeakReference<View>(view);
	}

	public float getAlpha() {
		return mAlpha;
	}

	public void setAlpha(float alpha) {
		if (mAlpha != alpha) {
			mAlpha = alpha;
			View view = mView.get();
			if (view != null) {
				view.invalidate();
			}
		}
	}

	public float getPivotX() {
		return mPivotX;
	}

	public void setPivotX(float pivotX) {
		if (!mHasPivot || mPivotX != pivotX) {
			prepareForUpdate();
			mHasPivot = true;
			mPivotX = pivotX;
			invalidateAfterUpdate();
		}
	}

	public float getPivotY() {
		return mPivotY;
	}

	public void setPivotY(float pivotY) {
		if (!mHasPivot || mPivotY != pivotY) {
			prepareForUpdate();
			mHasPivot = true;
			mPivotY = pivotY;
			invalidateAfterUpdate();
		}
	}

	public float getRotation() {
		return mRotationZ;
	}

	public void setRotation(float rotation) {
		if (mRotationZ != rotation) {
			prepareForUpdate();
			mRotationZ = rotation;
			invalidateAfterUpdate();
		}
	}

	public float getRotationX() {
		return mRotationX;
	}

	public void setRotationX(float rotationX) {
		if (mRotationX != rotationX) {
			prepareForUpdate();
			mRotationX = rotationX;
			invalidateAfterUpdate();
		}
	}

	public float getRotationY() {
		return mRotationY;
	}

	public void setRotationY(float rotationY) {
		if (mRotationY != rotationY) {
			prepareForUpdate();
			mRotationY = rotationY;
			invalidateAfterUpdate();
		}
	}

	public float getScaleX() {
		return mScaleX;
	}

	public void setScaleX(float scaleX) {
		if (mScaleX != scaleX) {
			prepareForUpdate();
			mScaleX = scaleX;
			invalidateAfterUpdate();
		}
	}

	public float getScaleY() {
		return mScaleY;
	}

	public void setScaleY(float scaleY) {
		if (mScaleY != scaleY) {
			prepareForUpdate();
			mScaleY = scaleY;
			invalidateAfterUpdate();
		}
	}

	public int getScrollX() {
		View view = mView.get();
		if (view == null) {
			return 0;
		}
		return view.getScrollX();
	}

	public void setScrollX(int value) {
		View view = mView.get();
		if (view != null) {
			view.scrollTo(value, view.getScrollY());
		}
	}

	public int getScrollY() {
		View view = mView.get();
		if (view == null) {
			return 0;
		}
		return view.getScrollY();
	}

	public void setScrollY(int value) {
		View view = mView.get();
		if (view != null) {
			view.scrollTo(view.getScrollX(), value);
		}
	}

	public float getTranslationX() {
		return mTranslationX;
	}

	public void setTranslationX(float translationX) {
		if (mTranslationX != translationX) {
			prepareForUpdate();
			mTranslationX = translationX;
			invalidateAfterUpdate();
		}
	}

	public float getTranslationY() {
		return mTranslationY;
	}

	public void setTranslationY(float translationY) {
		if (mTranslationY != translationY) {
			prepareForUpdate();
			mTranslationY = translationY;
			invalidateAfterUpdate();
		}
	}

	public float getX() {
		View view = mView.get();
		if (view == null) {
			return 0;
		}
		return view.getLeft() + mTranslationX;
	}

	public void setX(float x) {
		View view = mView.get();
		if (view != null) {
			setTranslationX(x - view.getLeft());
		}
	}

	public float getY() {
		View view = mView.get();
		if (view == null) {
			return 0;
		}
		return view.getTop() + mTranslationY;
	}

	public void setY(float y) {
		View view = mView.get();
		if (view != null) {
			setTranslationY(y - view.getTop());
		}
	}

	private void prepareForUpdate() {
		View view = mView.get();
		if (view != null) {
			computeRect(mBefore, view);
		}
	}

	private void invalidateAfterUpdate() {
		View view = mView.get();
		if (view == null || view.getParent() == null) {
			return;
		}

		final RectF after = mAfter;
		computeRect(after, view);
		after.union(mBefore);

		((View) view.getParent()).invalidate((int) Math.floor(after.left),
				(int) Math.floor(after.top), (int) Math.ceil(after.right),
				(int) Math.ceil(after.bottom));
	}

	private void computeRect(final RectF r, View view) {
		// compute current rectangle according to matrix transformation
		final float w = view.getWidth();
		final float h = view.getHeight();

		// use a rectangle at 0,0 to make sure we don't run into issues with
		// scaling
		r.set(0, 0, w, h);

		final Matrix m = mTempMatrix;
		m.reset();
		transformMatrix(m, view);
		mTempMatrix.mapRect(r);

		r.offset(view.getLeft(), view.getTop());

		// Straighten coords if rotations flipped them
		if (r.right < r.left) {
			final float f = r.right;
			r.right = r.left;
			r.left = f;
		}
		if (r.bottom < r.top) {
			final float f = r.top;
			r.top = r.bottom;
			r.bottom = f;
		}
	}

	private void transformMatrix(Matrix m, View view) {
		final float w = view.getWidth();
		final float h = view.getHeight();
		final boolean hasPivot = mHasPivot;
		final float pX = hasPivot ? mPivotX : w / 2f;
		final float pY = hasPivot ? mPivotY : h / 2f;

		final float rX = mRotationX;
		final float rY = mRotationY;
		final float rZ = mRotationZ;
		if ((rX != 0) || (rY != 0) || (rZ != 0)) {
			final Camera camera = mCamera;
			camera.save();
			camera.rotateX(rX);
			camera.rotateY(rY);
			camera.rotateZ(-rZ);
			camera.getMatrix(m);
			camera.restore();
			m.preTranslate(-pX, -pY);
			m.postTranslate(pX, pY);
		}

		final float sX = mScaleX;
		final float sY = mScaleY;
		if ((sX != 1.0f) || (sY != 1.0f)) {
			m.postScale(sX, sY);
			final float sPX = -(pX / w) * ((sX * w) - w);
			final float sPY = -(pY / h) * ((sY * h) - h);
			m.postTranslate(sPX, sPY);
		}

		m.postTranslate(mTranslationX, mTranslationY);
	}

	@Override
	protected void applyTransformation(float interpolatedTime, Transformation t) {
		View view = mView.get();
		if (view != null) {
			t.setAlpha(mAlpha);
			transformMatrix(t.getMatrix(), view);
		}
	}
}
